import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CommonTokenStream;

class Interpreter {

	public static void main(final String[] args) throws Exception
	{
		long totalQueryTime = 0;
		final ArrayList<Long> queryRunTimes = new ArrayList<Long>();
		int numQueriesRun = 0;

		try
		{
			DBConfig.readDbConfig();
		}
		catch(IOException e)
		{
			if(DBConfig.DEBUG)
			{
				e.printStackTrace();
			}
			System.out.println("Unable to read db configuration file " + DBConfig.DBCONFIG_FILENAME + ".");
			System.out.println("Attempting to run with default configuration settings.");
		}

		try
		{
			final CatalogReader foo = new CatalogReader("Catalog.xml");
			final Map<String, TableData> res = foo.getCatalog();
			if(DBConfig.DEBUG)
			{
				System.out.println(CatalogReader.printCatalog(res));
			}

			final InputStreamReader converter = new InputStreamReader(System.in);
			final BufferedReader in = new BufferedReader(converter);

			// System.out.format("\nSQL> ");
			// String soFar = in.readLine() + " ";

			String soFar = null;

			// loop forever, or until someone asks to quit
			while(true)
			{
				// If the user input a single line with multiple queries then
				// run all of the queries before printing a new prompt.
				// This may not be the best way to code this. Need to test more.
				// if (soFar != null && soFar.indexOf(';') != -1) {
				// System.out.format("\nSQL> ");
				// }
				System.out.format("\nSQL> ");

				// Perhaps should change this
				if(soFar == null)
				{
					soFar = in.readLine() + " ";
				}

				// keep on reading from standard in until we hit a ";"
				while(soFar.indexOf(';') == -1)
				{
					// JES - the extra space may be unnecessary
					soFar += (in.readLine() + " ");
				}

				// split the string
				String toParse = soFar.substring(0, soFar.indexOf(';') + 1);
				soFar = soFar.substring(soFar.indexOf(';') + 1, soFar.length());
				toParse = toParse.toLowerCase();

				// parse it
				final ANTLRStringStream parserIn = new ANTLRStringStream(toParse);
				final SQLLexer lexer = new SQLLexer(parserIn);
				final CommonTokenStream tokens = new CommonTokenStream(lexer);
				final SQLParser parser = new SQLParser(tokens);

				// if we got a quit
				if(parser.parse() == false)
				{
					break;
				}

				// print the results
				final ArrayList<Expression> mySelect = parser.getSELECT();
				final Map<String, String> myFrom = parser.getFROM();
				final Expression where = parser.getWHERE();
				final String att = parser.getGROUPBY();
				ArrayList<SortExpression> orderBy = parser.getORDERBY();

				if(DBConfig.DEBUG)
				{
					System.out.println("RESULT OF PARSING");
					
					System.out.println("Expressions in SELECT:");
					for(final Expression e : mySelect)
					{
						if(DBConfig.DEBUG)
						{
							System.out.println("\t" + e.print());
						}
					}
					
					System.out.println("Tables in FROM:");
					System.out.println("\t" + myFrom);
					
					System.out.println("WHERE clause:");
					if(where != null)
					{
						if(DBConfig.DEBUG)
						{
							System.out.println("\t" + where.print());
						}
					}
					
					System.out.println("GROUPING att:");
					if(att != null)
					{
						if(DBConfig.DEBUG)
						{
							System.out.println("\t" + att);
						}
					}
					
					System.out.println("ORDER BY: " + orderBy.toString());
				}

				// Semantic check (2)
				// Validate that all tables in the from clause exist in the
				// catalog.
				if(!checkTables(parser, res))
				{
					continue;
				}

				// Semantic check (3)
				// Validate that all attributes referenced in the query exist
				// and are correctly attached to tables in the query.
				if(!checkAttributes(parser, res))
				{
					continue;
				}

				// Semantic check (4)
				// Validate that if this query is an aggregate query, the only
				// attributes referenced in the select list except aggregate
				// function input are in the group by clause.
				// Validate that there are no nested aggregate functions.
				if(!checkGroupBy(parser, res))
				{
					continue;
				}

				// Semantic check (1)
				// Validate that all expressions are properly typed.
				// THIS MAY REQUIRE AN ENHANCEMENT TO VALIDATE THAT THE RETURN
				// TYPE OF ANY EXPRESSION IN THE SELECT IS ONLY OF TYPE Str Int
				// Float
				// BOOLEAN TYPES MAY BE INVALID TO SELECT
				if(!checkExpressionTypes(parser, res))
				{
					continue;
				}

				if(DBConfig.DEBUG)
				{
					System.out.println("Query passes semantic checks.");
				}
				
				//continue;
				
				if(parser.isAggregation() && parser.isOrdered())
				{
					System.out.println("I'm sorry. Currently queries with and order by and group by are unsupported.");
					continue;
				}
				
				// Print the query that is being executed
				if(DBConfig.ECHO_QUERY)
				{
					System.out.println("Executing: " + toParse);
				}

				RAExpression ra = convertParserToRA(parser, res);

				// Optimize the RAExpression
				ra = optimize(ra, res);

				final long queryStartTime = System.currentTimeMillis();
				// JES - temporarily commented since I don't have a means to
				// execute anything.
				ArrayList<Expression> necessaryColumns = getReferencedColumns(parser);
				ra.pushdownReferencedColumnsList(necessaryColumns);
				final RAOutput queryOutput = ra.execute();
				final long queryFinishTime = System.currentTimeMillis();
				final long queryRunTime = queryFinishTime - queryStartTime;

				totalQueryTime += queryRunTime;
				queryRunTimes.add(queryRunTime);
				numQueriesRun++;

				// JES - temp commented
				printQueryResults(queryOutput.outFile);
				System.out.println("Query run time: " + queryRunTime);

				// JES - temp commented
				if(DBConfig.DELETE_TEMP_CPP_FILES && !queryOutput.outFile.endsWith(".tbl"))
				{
					final File f = new File(queryOutput.outFile);
					f.delete();
				}
			}

			System.out.println("Ran " + numQueriesRun + " queries successfully.");
			System.out.println("Total query execution time was " + totalQueryTime + ".");
			System.out.println("Run time broken down by query:");
			for(int i = 0; i < numQueriesRun; i++)
			{
				System.out.println("\tQuery " + (i + 1) + ": " + queryRunTimes.get(i));
			}
			System.out.println("Total run time scalled by time to execute Runner: " + totalQueryTime / 21196);
		}
		catch(final Exception e)
		{
			e.printStackTrace();
			System.out.println("Error! Exception: " + e);
		}
	}

	private static ArrayList<Expression> getReferencedColumns(SQLParser parser)
	{
		ArrayList<Expression> select = parser.getSELECT();
		Expression where = parser.getWHERE();
		String groupBy = parser.getGROUPBY();
		ArrayList<SortExpression> orderBy = parser.getORDERBY();

		ArrayList<Expression> referencedColumns = new ArrayList<Expression>();
		for(Expression e : select)
		{
			ArrayList<Expression> selectColumns = e.getIdentifiers();
			if(selectColumns != null)
			{
				referencedColumns.addAll(selectColumns);
			}
		}
		
		if(where != null)
		{
			ArrayList<Expression> whereColumns = where.getIdentifiers();
			if(whereColumns != null)
			{
				referencedColumns.addAll(whereColumns);
			}
		}

		if(groupBy != null)
		{
			Expression e = new Expression("identifier");
			e.setValue(groupBy);
			referencedColumns.add(e);
		}

		if(orderBy != null)
		{
			for(SortExpression se : orderBy)
			{
				Expression e = se.getExpression();
				ArrayList<Expression> orderByColumns = e.getIdentifiers();
				if(orderByColumns != null)
				{
					referencedColumns.addAll(orderByColumns);
				}
			}
		}

		return referencedColumns;
	}

	private static RAExpression optimize(RAExpression ra, Map<String, TableData> catalog)
	{
		Map<String, TableData> tempCatalog = new HashMap<String, TableData>();
		RAStatistics stats = ra.estimateCost(catalog, tempCatalog);
		long cost = stats.getCost();
		long card = stats.getCardinality();

		if(DBConfig.DEBUG)
		{
			System.out.println("Initial join plan: " + ra.joinPlanToString());
			System.out.println("RA expression cost estimate: " + cost);
			System.out.println("RA expression card estimate: " + card);
		}

		RAExpression bestPlan = ra;
		long bestCost = cost;

		ArrayList<RAExpression> transformations = ra.getJoinTransformations();
		while(transformations != null && transformations.size() >= 1)
		{
			RAExpression newBest = transformations.get(0);
			RAStatistics newBestStats = newBest.estimateCost(catalog, new HashMap<String, TableData>());
			long newBestCost = newBestStats.getCost();
			long newBestCard = newBestStats.getCardinality();
			if(DBConfig.DEBUG && newBestCard != card)
			{
				System.err.println("Unexpected cardinality change in transformations:\nExpected Card: " + card
						+ "\nEstimated Card: " + newBestCard);
			}

			for(int i = 1; i < transformations.size(); i++)
			{
				RAExpression transformation = transformations.get(i);
				RAStatistics tStats = transformation.estimateCost(catalog, new HashMap<String, TableData>());
				long tCost = tStats.getCost();
				long tCard = tStats.getCardinality();
				if(DBConfig.DEBUG && tCard != card)
				{
					System.err.println("Unexpected cardinality change in transformations:\nExpected Card: " + card
							+ "\nEstimated Card: " + tCard);
				}

				if(tCost < newBestCost)
				{
					newBest = transformation;
					newBestStats = tStats;
					newBestCost = tCost;
					newBestCard = tCard;
				}
			}

			if(newBestCost < bestCost)
			{
				if(DBConfig.DEBUG)
				{
					System.out.println("Found a better plan!");
					System.out.println("Better plan: " + newBest.joinPlanToString());
				}
				bestPlan = newBest;
				bestCost = newBestCost;
				transformations = bestPlan.getJoinTransformations();
			}
			else
			{
				break;
			}
		}

		// Need to make sure that all of the joins are flipped so that the lower
		// cardinality is on the left!
		// JES TODO: Make sure joins are flipped optimally

		if(DBConfig.DEBUG)
		{
			System.out.println("Best plan: " + bestPlan.joinPlanToString());
			System.out.println("Best cost: " + bestCost);
			System.out.println("Optimization improvement: " + (cost - bestCost));
		}
		return bestPlan;
	}

	private static void printQueryResults(final String outFile)
	{
		String line;
		System.out.println("Query ran successfully. The first 30 rows returned are:");
		try
		{
			final DataInputStream in = new DataInputStream(new FileInputStream(outFile));
			final BufferedReader myReader = new BufferedReader(new InputStreamReader(in));

			for(int i = 0; i < 30; i++)
			{
				line = myReader.readLine();
				if(line == null)
				{
					break;
				}
				System.out.println(line);
			}
			System.out.println("");
		}
		catch(Exception e)
		{
			e.printStackTrace();
			throw new RuntimeException("Could not open the file!");
		}
	}
	
	/**
	 * Converts the parser input into an initial RAExpression which will be further optimized.
	 * @param parser
	 * @param catalog
	 * @return
	 */
	private static RAExpression convertParserToRA(final SQLParser parser, final Map<String, TableData> catalog)
	{
		// Get parser info
		ArrayList<Expression> select = parser.getSELECT();
		Map<String, String> from = parser.getFROM();
		Expression where = parser.getWHERE();
		String groupBy = parser.getGROUPBY();
		ArrayList<SortExpression> orderBy = parser.getORDERBY();
		
		int numTables = from.entrySet().size();
		
		// Get all of the column identifiers used by this query
		ArrayList<Expression> allIdentifiers = Interpreter.getReferencedColumns(parser);
		
		// Convert the where clause to disjunctions
		ArrayList<Expression> allWhereDisjunctions;
		if(where != null)
		{
			allWhereDisjunctions = where.toDisjunctions();
		}
		else
		{
			allWhereDisjunctions = new ArrayList<Expression>();
		}
		int disjunctionCount = allWhereDisjunctions.size();
		
		// Create a map of table correlations to disjunctions that only reference those tables
		Map<String, ArrayList<Expression>> singleTableWhereClauses = new HashMap<String, ArrayList<Expression>>();
		ArrayList<Expression> joinWhereClauses = new ArrayList<Expression>(disjunctionCount);
		
		// For each disjunction, get all of the correlations it references.
		// If these is only one add it to SingleTableWhereClauses.
		// If there are more than one, add it to joinWhereClauses.
		for(Expression whereDisjunction : allWhereDisjunctions)
		{
			Set<String> referencedCorrelations = whereDisjunction.getSetOfCorrelations();
			int correlationCount = referencedCorrelations.size();
			if(correlationCount > 1)
			{
				joinWhereClauses.add(whereDisjunction);
			}
			else if(correlationCount == 1)
			{
				String correlation = (String) referencedCorrelations.toArray()[0];
				if(singleTableWhereClauses.containsKey(correlation))
				{
					singleTableWhereClauses.get(correlation).add(whereDisjunction);
				}
				else
				{
					ArrayList<Expression> singleTableDisjunctions = new ArrayList<Expression>();
					singleTableDisjunctions.add(whereDisjunction);
					singleTableWhereClauses.put(correlation, singleTableDisjunctions);
				}
			}
			else if(correlationCount == 0)
			{
				// This is some expression that can be evaluated right away and applies to everything.
				// It either evaluates to true or false.
				// If it evaluates to true then it can be ignored the rest of the query.
				// If it evaluates to false there is no need to even run the query.
				
				throw new RuntimeException("Support for AND disjunctions that are constant has not yet been implemented.");
			}
			else
			{
				throw new RuntimeException("ERROR: Correlation count not \">1\", \"==1\", or \"==0\".");
			}
		}
		
		// Create a map of table correlations to new RASelections and RAReadBaseTables.
		Map<String, RAExpression> singleTableRA = new HashMap<String, RAExpression>();
		for(Map.Entry<String, String> mapEntry : from.entrySet())
		{
			String correlation = mapEntry.getKey();
			String table = mapEntry.getValue();
			
//			if(singleTableWhereClauses.containsKey(correlation))
//			{
				// Add an RASelection to the map
				Expression myWhere = Expression.reJunct(singleTableWhereClauses.get(correlation));
				RASelection raSelection = new RASelection(catalog, null, from, correlation, table, myWhere, false);
				singleTableRA.put(correlation, raSelection);
//			}
//			else
//			{
//				// Add an RAReadBaseTable to the map
//				RAReadBaseTable raReadBaseTable = new RAReadBaseTable(catalog, correlation, table);
//				singleTableRA.put(correlation, raReadBaseTable);
//			}
		}
		
		// Build up a join tree from all of the single table operations
		RAExpression raWithJoin = null;
		if(numTables == 1)
		{
			raWithJoin = (RAExpression) singleTableRA.values().toArray()[0];
		}
		else if(numTables > 1)
		{
			raWithJoin = initializeRAJoin(singleTableRA, catalog, from);
		}
		else
		{
			throw new RuntimeException("ERROR: numTables not \"==1\" and not \">1\"");
		}
		
		// Push down the join predicates
		raWithJoin.pushdownJoinPredicates(joinWhereClauses);
		
		// Put a group by on top if necessary
		RAExpression raWithGrouping = null;
		if(parser.isAggregation())
		{
			raWithGrouping = new RAGrouping(catalog, select, from, groupBy, raWithJoin);
		}
		else
		{
			raWithGrouping = raWithJoin;
		}
		
		
		// Put an order by on top if necessary
		RAExpression raWithOrder = null;
		if(parser.isOrdered())
		{
			raWithOrder = new RAOrder(catalog, select, from, orderBy, raWithGrouping);
		}
		else
		{
			raWithOrder = raWithGrouping;
		}

		// JES TODO: The current implementation of RAGrouping supports a selection
		// predicate, so it is possible to combine a selection into a grouping.
		// This capability should be added to an order by. This should be a final optimization
		// step.
		
		raWithOrder.pushdownReferencedColumnsList(allIdentifiers);
		
		raWithOrder.applySelectList(select);
		
		return raWithOrder;
	}
	
	private static RAExpression initializeRAJoin(Map<String, RAExpression> singleTableRA,
			final Map<String, TableData> catalog, Map<String, String> from)
	{
		int numTables = singleTableRA.size();
		if(numTables < 2)
		{
			throw new RuntimeException("ERROR: Cannot initialize an RAJoin with less than 2 tables.");
		}
		
		ArrayList<RAExpression> sortedRA = new ArrayList<RAExpression>(numTables);
		sortedRA.addAll(singleTableRA.values());
		Collections.sort(sortedRA, new Comparator<RAExpression>() {
			public int compare(RAExpression arg0, RAExpression arg1)
			{
				long card0 = arg0.estimateCost(catalog, new HashMap<String, TableData>()).getCardinality();
				long card1 = arg1.estimateCost(catalog, new HashMap<String, TableData>()).getCardinality();
				return (card0 < card1) ? -1 : (card0 == card1) ? 0 : 1;
			}
		});
		
		RAJoin join = new RAJoin(catalog, sortedRA.get(numTables - 2), sortedRA.get(numTables - 1), from);
		for(int raIdx = numTables - 3; raIdx >= 0; raIdx--)
		{
			join = new RAJoin(catalog, sortedRA.get(raIdx), join, from);
		}
		
		return join;
	}
	
//	private static RAExpression preGroupByRA(final SQLParser parser, final Map<String, TableData> catalog)
//	{
//		// final ArrayList<Expression> select = parser.getSELECT();
//		final Map<String, String> from = parser.getFROM();
//		// final Expression where = parser.getWHERE();
//		// final String groupBy = parser.getGROUPBY();
//		
//		/*
//		 * ArrayList<Expression> projectedAtts = new ArrayList<Expression>();
//		 * for (Expression e : select) e.computeProjectedAtts(projectedAtts); if
//		 * (where != null) where.computeProjectedAtts(projectedAtts);
//		 */
//		
//		
//		final int tableCount = from.size();
//		if(tableCount == 1)
//		{
//			// Single Table queries should work now
//			if(parser.isAggregation())
//			{
//				return new RAGrouping(parser, catalog);
//			}
//			else
//			{
//				return new RASelection(parser, catalog, true);
//			}
//		}
//		else if(tableCount > 1)
//		{
//			// This code needs to be updated
//			if(parser.isAggregation())
//			{
//				return new RAGrouping(parser, catalog);
//			}
//			return new RAJoin(parser, catalog);
//		}
//		else
//		{
//			// tableCount <= 0
//			throw new RuntimeException("Attempted to convert what should have been"
//					+ " a valid SQL query to an RA expression but non-positive table count found.");
//		}
//		
//	}

	/**
	 * Validates that there are no type mismatches in any of the expressions.
	 * 
	 * @param parser
	 * @return true if no type mismatches, false otherwise
	 */
	private static boolean checkExpressionTypes(final SQLParser parser, final Map<String, TableData> catalog)
	{
		final ArrayList<Expression> select = parser.getSELECT();
		final Map<String, String> tables = parser.getFROM();
		final Expression where = parser.getWHERE();
		final ArrayList<SortExpression> orderBy = parser.getORDERBY();

		for(final Expression e : select)
		{
			if(!e.checkType(tables, catalog))
			{
				System.out.println("Select clause expression \"" + e.print() + "\" of type \"" + e.getType()
						+ "\" contains a type mismatch.");
				return false;
			}
		}

		if(where != null && !where.checkType(tables, catalog))
		{
			System.out.println("Where clause expression \n" + where.print() + "\" of type \"" + where.getType()
					+ "\" contains a type mismatch.");
			return false;
		}

		// the from clause does not need to be type checked
		// the group by clause is limited by the parser to be a single attribute
		// and hence is properly typed
		if(orderBy != null)
		{
			for(final SortExpression se : orderBy)
			{
				Expression e = se.getExpression();
				if(!e.checkType(tables, catalog))
				{
					System.out.println("Order by clause expression \"" + e.print() + "\" of type \"" + e.getType()
							+ "\" contains a type mismatch.");
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Returns true if the catalog contains each table referenced in the from
	 * clause of the parser. The parser validates that the correlation names of
	 * the tables are unique, so this only validates that the tables are in the
	 * catalog.
	 * 
	 * @param parser
	 * @param catalog
	 * @return
	 */
	private static boolean checkTables(final SQLParser parser, final Map<String, TableData> catalog)
	{
		final Map<String, String> tables = parser.getFROM();

		for(final String table : tables.values())
		{
			if(!catalog.containsKey(table))
			{
				System.out.println("Table \"" + table + "\" does not exist.");
				return false;
			}
		}

		return true;
	}

	/**
	 * Returns true if all attributes in the query parsed by parser are valid
	 * table attributes. This function should be called after validating that
	 * all tables referenced in the query exist in the catalog. TO DO: It might
	 * be good to extend the Expression class to include the type since we are
	 * doing a catalog look up here.
	 * 
	 * @param parser
	 * @param catalog
	 * @return
	 */
	private static boolean checkAttributes(final SQLParser parser, final Map<String, TableData> catalog)
	{
		final ArrayList<Expression> select = parser.getSELECT();
		final Map<String, String> tables = parser.getFROM();
		final Expression where = parser.getWHERE();
		final String groupBy = parser.getGROUPBY();
		final ArrayList<SortExpression> orderBy = parser.getORDERBY();

		// Check the select list expressions
		for(final Expression e : select)
		{
			if(!checkExpressionAttributes(e, tables, catalog))
			{
				return false;
			}
		}

		// Check the where clause expressions
		if(where != null)
		{
			if(!checkExpressionAttributes(where, tables, catalog))
			{
				return false;
			}
		}

		// Check the group by attribute
		if(groupBy != null)
		{
			if(!attributeExists(tables, catalog, groupBy))
			{
				return false;
			}
		}

		// Further extensions to SQL would go here:

		// Check the having clause attributes

		// Check the order by clause attribute
		if(orderBy != null)
		{
			for(SortExpression se : orderBy)
			{
				Expression e = se.getExpression();
				if(!checkExpressionAttributes(e, tables, catalog))
				{
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Validates that all attributes referenced in expression "e" are correlated
	 * to a catalog table in "tables" that contains the attribute.
	 * 
	 * @param e
	 * @param catalog
	 * @return
	 */
	private static boolean checkExpressionAttributes(final Expression e, final Map<String, String> tables,
			final Map<String, TableData> catalog)
	{
		final String type = e.getType();

		// Base case - type is an identifier
		if(type.equals("identifier"))
		{
			final String value = e.getValue();
			return attributeExists(tables, catalog, value);
		}

		// Unary Operator case
		for(final String unaryType : Expression.unaryTypes)
		{
			if(e.getType().equals(unaryType))
			{
				return checkExpressionAttributes(e.getSubexpression(), tables, catalog);
			}
		}

		// Binary Operator case
		for(final String binaryType : Expression.binaryTypes)
		{
			if(e.getType().equals(binaryType))
			{
				if(!checkExpressionAttributes(e.getSubexpression("left"), tables, catalog))
				{
					return false;
				}
				return checkExpressionAttributes(e.getSubexpression("right"), tables, catalog);
			}
		}

		// Other identifiers are non recursive and can't include attributes
		return true;
	}

	/**
	 * Returns true if the attribute "attribute" exists in table "table".
	 * 
	 * @param table
	 * @param attribute
	 * @return
	 * @throws InvalidFunctionInputException
	 */
	private static boolean attributeExists(final Map<String, String> tables, final Map<String, TableData> catalog,
			final String attribute)
	{
		if(tables == null || catalog == null || attribute == null)
		{
			throw new RuntimeException("Called function attributeExists with invalid input.");
		}

		final CorrelatedAttribute ca = CorrelatedAttribute.parseCorrelatedAttribute(attribute);
		if(ca == null)
		{
			System.out.println("Attribute \"" + attribute + "\" is not a valid correlated attribute.");
			return false;
		}

		final String table = tables.get(ca.getCorrelation());
		if(table == null)
		{
			System.out.println("Attribute \"" + attribute + "\" has undefined correlation \"" + ca.getCorrelation()
					+ "\".");
			return false;
		}

		if(catalog.get(table).getAttInfo(ca.getAttribute()) == null)
		{
			System.out.println("Attribute \"" + attribute + "\" does not exist in table \"" + table + "\".");
			return false;
		}

		return true;
	}

	/**
	 * Returns true if the query in "parser" is valid with respect to its
	 * grouping attribute or any grouping functions. If the group by clause is
	 * used, then all attributes referenced in the select list must be contained
	 * in the group by clause except those referenced in aggregate functions. If
	 * there is no grouping attribute but an aggregate function is used then
	 * there can be no attributes in the select list except those in the
	 * grouping functions. Additionally the where clause may not use aggregate
	 * functions.
	 * 
	 * @param parser
	 * @param catalog
	 * @return
	 */
	private static boolean checkGroupBy(final SQLParser parser, final Map<String, TableData> catalog)
	{
		// Determine if this query is an aggregation query.
		// If the group by clause is not NULL or there is an aggregation
		// function in the select list then this is a group by query.
		// Additionally if the having clause is implemented then this is a group
		// by query.
		final ArrayList<Expression> select = parser.getSELECT();
		final Expression where = parser.getWHERE();
		final String groupBy = parser.getGROUPBY();
		final ArrayList<SortExpression> orderBy = parser.getORDERBY();

		// Validate that the where clause does not include
		// any aggregation functions.
		if(where != null)
		{
			if(where.includesAggregateFunction())
			{
				System.out.println("Where clause \"" + where.print()
						+ "\" is invalid because it contains an aggregate function.");
				return false;
			}
		}

		// replaces the code below
		if(!parser.isAggregation())
		{
			return true;
			/*
			 * if (groupBy != null) { isAggregation = true; } else { for
			 * (Expression e : select) { if (e.includesAggregateFunction()) {
			 * isAggregation = true; break; } } // If the query is not an
			 * aggregation query then the semantic check passed if
			 * (!isAggregation) return true; }
			 */
		}

		// If control flow reaches this pointer then the query is an
		// aggregation query with a valid where clause - proceed with semantic
		// checks of select clause.

		// For each attribute that is not contained within a grouping function
		// validate that it is in the group by clause.
		for(final Expression e : select)
		{
			if(!e.checkGroupingAttributes(groupBy))
			{
				System.out.println("Select list expression \"" + e.print()
						+ "\" references an attribute not included in the group by clause.");
				return false;
			}
		}

		if(orderBy != null)
		{
			for(SortExpression se : orderBy)
			{
				Expression e = se.getExpression();
				if(!e.checkGroupingAttributes(groupBy))
				{
					System.out.println("Order by expression \"" + e.print()
							+ "\" references an attribute not included in the group by clause.");
					return false;
				}
			}
		}

		// Validate that there are no nested aggregate functions.
		for(final Expression e : select)
		{
			if(!e.checkAggregateFunctionInput())
			{
				System.out.println("Select list expression \"" + e.print() + "\" contains nested aggregate functions.");
				return false;
			}
		}

		if(orderBy != null)
		{
			for(final SortExpression se : orderBy)
			{
				Expression e = se.getExpression();
				if(!e.checkAggregateFunctionInput())
				{
					System.out
							.println("Order by expression \"" + e.print() + "\" contains nested aggregate functions.");
					return false;
				}
			}
		}

		return true;
	}

	// Additional things to consider adding to query checking:
	// where clause must not contain grouping attributes
	// there are probably tons of other things

}